Class and Inheritance

Explanation in a easy way:

  • class: a structure for more complicated data that contain arbitrary content (just a structure, doesn't actually fill in the content)
  • instance: a specific copy of the class that does contain all of the content
  • For example, I have a class called Pokemon, and it contains some basic information that is needed to be filled in, like Pokemon's name, attack, defense, etc. Now I fill in some data, Pokemon('Pikachu', 100, 50), and then a Pikachu instance is just created.

In [2]:
# Define a class named Pokemon
class Pokemon():
    def __init__(self, name, attack, defence):
        self.name = name
        self.attack = attack
        self.defence = defence
        print('Hello world')
    
    def poko_name(self):
        return self.name
    
    def poko_state(self):
        return self.attack, self.defence
    
    def __str__(self):
        return 'My name is %s, and my attack is %d, defence is %d.' %(self.name, self.attack, self.defence)

In [3]:
Pikachu = Pokemon('Pikachu', 100, 50)


Hello world

In [7]:
Pikachu.poko_name()


Out[7]:
'Pikachu'

In [6]:
Pikachu.poko_state()


Out[6]:
(100, 50)

In [23]:
Pikachu.name


Out[23]:
'Pikachu'
  • notice that a function in a class is called method

What is __init__?

  • When we create a Pokemon, we need to initialize with its basic information as mentioned. The __init__ method is a special Python function that is called when an instance of a class is first created.
  • When running the code Pikachu = Pokemon('Pikachu', 100, 50), the __init__ method is called with values Pikachu, "Pikachu", 100, and 50 for the variables self, name, attack and defence, respectively.

What is self?

  • self is the instance. In this case, self represents the Pikachu instance itself.

But there are 4 variables in __init__ method (self, name, attack, defence), why we only need to fill in 3 (e.g. Pikachu = Pokemon('Pikachu', 100, 50)) ?

  • This is a special behavior of Python: when you call a method of an instance, Python automatically figures out what self should be (from the instance) and passes it to the function.

In [45]:
Pikachu = Pokemon(Pikachu, 'Pikachu', 100, 50)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-45-88ce65354210> in <module>()
----> 1 Pikachu = Pokemon(Pikachu, 'Pikachu', 100, 50)

TypeError: __init__() takes 4 positional arguments but 5 were given

So a method in a class always have self as its first variable, which means getting the contents of the instance.


In [8]:
### To make it clear, here's an example.
### Standard way of calling `poko_name`:
Pikachu.poko_name()
### In the case, `self` represents the Pikachu instance itself, and is automatically passed into the function by Python.


Out[8]:
'Pikachu'

In [9]:
### Inconventional way:
Pokemon.poko_name(Pikachu)


Out[9]:
'Pikachu'

In [10]:
### if we didn't do that
Pokemon.poko_name()
### then there is a variable missed.


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-79d11dfba256> in <module>()
      1 ### if we didn't do that
----> 2 Pokemon.poko_name()
      3 ### then there is a variable missed.

TypeError: poko_name() missing 1 required positional argument: 'self'

What is __str__?

  • It's also a special method like __init__. (Actually, you will find that something in double underscores always has special function in Python.)
  • This method defines the format if print out.

In [11]:
### what __str__ does.
print(Pikachu)


My name is Pikachu, and my attack is 100, defence is 50.

Subclass

  • Sometimes a single class is not enough. For example, there are 18 types of the Pokemon, like grass, fire, water, etc.
  • We can make another class that is a Pokemon but is also specifically a electric type Pokemon. This gives us the structure from Pokemon but also any structure we want to specify for Electric type.

In [12]:
class Electric(Pokemon):
    def __init__(self, name, attack, defence):
        Pokemon.__init__(self, name, attack, defence)

In [13]:
Raichu = Electric('Raichu', 200, 100)
print(Raichu)


Hello world
My name is Raichu, and my attack is 200, defence is 100.
  • In the case, Electric use the structure from Pokemon. Methods from Pokemon can be directly used. We can also define specfic methods:

In [14]:
class Electric(Pokemon):
    def __init__(self, name, attack, defence):
        Pokemon.__init__(self, name, attack, defence)
        
    def __str__(self):
        return 'My name is %s, and my attack is %d, defence is %d. I am electric type.' %(self.name, self.attack, self.defence)

In [15]:
Raichu = Electric('Raichu', 200, 100)
Raichu.poko_state()


Hello world
Out[15]:
(200, 100)

In [16]:
### __str__ method from Pokemon is replaced in Electric.
print(Raichu)


My name is Raichu, and my attack is 200, defence is 100. I am electric type.

Inheritance

  • In previous example, we already know that class Electric(Pokemon) means Electric class inherits from Pokemon class. (Electric is a subclass of Pokemon.)
  • Actually, if we remian blank in parenthesis, Python will make it inherit form object.
  • To be clear, in first example, class Pokemon() is totally same as class Pokemon(object).

In [17]:
### check instance
isinstance(Raichu, Electric)


Out[17]:
True

In [18]:
isinstance(Raichu, Pokemon)


Out[18]:
True

In [19]:
isinstance(Pikachu, Electric)


Out[19]:
False

In [20]:
isinstance(Pikachu, Pokemon)


Out[20]:
True

Multiple Inheritance


In [21]:
# An example for multiple inheritance:
class Base():
    def __init__(self, value):
        self.value = value


class Times(Base):
    def __init__(self, value):
        Base.__init__(self, value)
        self.value *= 10


class Plus(Base):
    def __init__(self, value):
        Base.__init__(self, value)
        self.value += 5


class MultInher(Times, Plus):
    def __init__(self, value):
        Times.__init__(self, value)
        Plus.__init__(self, value)

In [22]:
result = MultInher(8)

Now take a guess what is the value of result.value?


In [24]:
"""









































"""


Out[24]:
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'

Someone's answer is 85?


In [26]:
# Let's check.
result = MultInher(8)
result.value


Out[26]:
13

Why is 13?

  • You might notice that in this case, __init__ method in the base class is executed twice. However, most of time, we want it to execute for only one time.

Super()

  • When you have multiple inheritance, the super() helps to ensure that the proper method resolution order (MRO) is followed when moving up the inheritance tree.
  • Super() is to find next class in MRO.
  • It is also a shortcut to allow you to access the base class of a derived class, without having to know or type the base class name.

In [27]:
# Define a simple class named A
class A():
    def __init__(self, a):
        print('enter A')
        self.a = a
        print(self.a)
        print('leave A')
    
    def method(self):
        print()
        print('method from A')
        print(self.a)

In [30]:
# Try 
instance = A(3)
instance.method()


enter A
3
leave A

method from A
3

In [34]:
# Define a class named B which is inherited from A.
class A():
    def __init__(self, a):
        print('enter A')
        self.a = a
        print(self.a)
        print('leave A')
    
    def method(self):
        print()
        print('method from A')
        print(self.a)

class B(A):
    def __init__(self, b):
        print('enter B')
        super(B, self).__init__('I give from B')
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')

In [35]:
instance = B(1)
instance.method()


enter B
enter A
I give from B
leave A
self.a is changed from B
leave B

method from A
self.a is changed from B

In [38]:
# Another example using super()
class B(A):
    def __init__(self, b):
        print('enter B')
        super().__init__('I give from B')
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')

In [39]:
instance = B(1)
instance.method()


enter B
enter A
I give from B
leave A
self.a is changed from B
leave B

method from A
self.a is changed from B

In [40]:
# Use inheritance method I mentioned at beginning.
class B(A):
    def __init__(self, b):
        print('enter B')
        A.__init__(self, 'I give from B')
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')

In [41]:
instance = B(1)
instance.method()


enter B
enter A
I give from B
leave A
self.a is changed from B
leave B

method from A
self.a is changed from B
  • In this case, the results are same.
  • super(B, self) is totally same as super().
  • In multiple inheritance case, results would be different by using super() and A.__init__(self)

In [49]:
class A():
    def __init__(self, a):
        print('enter A')
        super().__init__()
        self.a = a
        print(self.a)
        print('leave A')
    
    def method(self):
        print()
        print('method from A')
        print(self.a)

        
class B(A):
    def __init__(self, b1):
        print('enter B')
        super().__init__('I give from B')
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')
        

class C(A):
    def __init__(self, c1):
        print('enter C')
        super().__init__('I give from C')
        print('leave C')
    
    def method(self):
        print()
        print('method from C')
        print(self.a)
        
        
class D(B, C):
    def __init__(self, d):
        print('enter D')
        self.a = d
        print('initial value of self.a:', self.a)
        super().__init__('I give from D')
        print(self.a)
        print('leave D')

In [54]:
D.mro()


Out[54]:
[__main__.D, __main__.B, __main__.C, __main__.A, object]

In [55]:
instance = D(3)
instance.method()


enter D
initial value of self.a: 3
enter B
enter C
enter A
I give from C
leave A
leave C
self.a is changed from B
leave B
self.a is changed from B
leave D

method from C
self.a is changed from B

In [60]:
class D(C, B):
    def __init__(self, d):
        print('enter D')
        self.a = d
        print('initial value of self.a:', self.a)
        super().__init__('I give from D')
        print(self.a)
        print('leave D')

In [62]:
D.mro()


Out[62]:
[__main__.D, __main__.C, __main__.B, __main__.A, object]

In [63]:
class D(A, B):
    def __init__(self, d):
        print('enter D')
        self.a = d
        print('initial value of self.a:', self.a)
        super().__init__('I give from D')
        print(self.a)
        print('leave D')


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-63-082d39164cc6> in <module>()
----> 1 class D(A, B):
      2     def __init__(self, d):
      3         print('enter D')
      4         self.a = d
      5         print('initial value of self.a:', self.a)

TypeError: Cannot create a consistent method resolution
order (MRO) for bases B, A

In [64]:
class A():
    def __init__(self, a):
        print('enter A')
        super().__init__()
        self.a = a
        print(self.a)
        print('leave A')
    
    def method(self):
        print()
        print('method from A')
        print(self.a)

        
class B(A):
    def __init__(self, b1):
        print('enter B')
        super().__init__('I give from B')
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')
    
    def method(self):
        print()
        print('method from B')
        print(self.a)
        

class C(A):
    def __init__(self, c1):
        print('enter C')
        super().__init__('I give from C')
        print('leave C')
    
    def method(self):
        print()
        print('method from C')
        print(self.a)
        
        
class D(B, C):
    def __init__(self, d):
        print('enter D')
        self.a = d
        print('initial value of self.a:', self.a)
        super().__init__('I give from D')
        print(self.a)
        print('leave D')
        
    def method(self):
        print()
        print('method from D')
        print(self.a)

In [66]:
intsance = D(3)
D(3).method()


enter D
initial value of self.a: 3
enter B
enter C
enter A
I give from C
leave A
leave C
self.a is changed from B
leave B
self.a is changed from B
leave D
enter D
initial value of self.a: 3
enter B
enter C
enter A
I give from C
leave A
leave C
self.a is changed from B
leave B
self.a is changed from B
leave D

method from D
self.a is changed from B

In [68]:
class A():
    def __init__(self, a):
        print('enter A')
        print('value of self.a:', self.a)
        super().__init__('I give from A')
        print(self.a)
        print('leave A')
    
    def method(self):
        print()
        print('method from A')
        print(self.a)

        
class B():
    def __init__(self, b1):
        print('enter B')
        print('value of self.a:', self.a)
        super().__init__()
        self.a = 'self.a is changed from B'
        print(self.a)
        print('leave B')
    
    def method(self):
        print()
        print('method from B')
        print(self.a)
        

class C(A):
    def __init__(self, c1):
        print('enter C')
        print('value of self.a:', self.a)
        super().__init__('I give from C')
        print('leave C')
    
    def method(self):
        print()
        print('method from C')
        print(self.a)
        
        
class D(B):
    def __init__(self, d):
        print('enter D')
        self.a = d
        print('value of self.a:', self.a)
        super().__init__('I give from D')
        print(self.a)
        print('leave D')
        
    def method(self):
        print()
        print('method from D')
        print(self.a)

class E(C, D):
    def __init__(self, e):
        print('enter E')
        self.a = e
        print('value of self.a:', self.a)
        super().__init__('I give from E')
        print(self.a)
        print('leave E')
        
    def method(self):
        print()
        print('method from E')
        print(self.a)

In [70]:
e.__class__.__mro__


Out[70]:
(__main__.E, __main__.C, __main__.A, __main__.D, __main__.B, object)

In [69]:
e = E(3)
e.method()


enter E
value of self.a: 3
enter C
value of self.a: 3
enter A
value of self.a: 3
enter D
value of self.a: I give from A
enter B
value of self.a: I give from A
self.a is changed from B
leave B
self.a is changed from B
leave D
self.a is changed from B
leave A
leave C
self.a is changed from B
leave E

method from E
self.a is changed from B

In [71]:
class F(E, C):
    def __init__(self, f):
        print('enter F')

In [72]:
F(3).__class__.__mro__


enter F
Out[72]:
(__main__.F,
 __main__.E,
 __main__.C,
 __main__.A,
 __main__.D,
 __main__.B,
 object)

In [73]:
class F(E, C):
    def __init__(self, f):
        print('enter F')
        super(F, self).__init__('I give from F')

In [74]:
F(3).__class__.__mro__


enter F
enter E
value of self.a: I give from F
enter C
value of self.a: I give from F
enter A
value of self.a: I give from F
enter D
value of self.a: I give from A
enter B
value of self.a: I give from A
self.a is changed from B
leave B
self.a is changed from B
leave D
self.a is changed from B
leave A
leave C
self.a is changed from B
leave E
Out[74]:
(__main__.F,
 __main__.E,
 __main__.C,
 __main__.A,
 __main__.D,
 __main__.B,
 object)

In [75]:
class F(E, C, B):
    def __init__(self, f):
        print('enter F')
        super(F, self).__init__('I give from F')

In [76]:
F.mro()


Out[76]:
[__main__.F,
 __main__.E,
 __main__.C,
 __main__.A,
 __main__.D,
 __main__.B,
 object]

In [77]:
class Base(object):
    def __init__(self, value):
        self.value = value
        

class TimesFive(Base):
    def __init__(self, value):
        super(TimesFive, self).__init__(value)
        self.value *= 5


class PlusTwo(Base):
    def __init__(self, value):
        super(PlusTwo, self).__init__(value)
        self.value += 2


class TestOne(TimesFive, PlusTwo):
    def __init__(self, value):
        super(TestOne, self).__init__(value)

In [80]:
test = TestOne(4)

In [81]:
# test.value???
"""









































"""


Out[81]:
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'

In [82]:
test.value


Out[82]:
30